#!/usr/bin/perl

use strict;
use warnings;

use Getopt::Long;
use Pod::Usage;
use FindBin;
use lib "$FindBin::Bin/../lib";

use Socket;
use POSIX qw/errno_h/;

use POE qw/
    Component::Client::Keepalive
    Component::Connection::Keepalive
    Wheel::ReadWrite
    Wheel::SocketFactory
    Filter::Stream
    /;
use Net::Rendezvous::Publish;

my $local_name    = 'daap-proxy';
my $local_port    = 9999;
my $remote_server = 'localhost';
my $remote_port   = 3689;
my $help          = 0;
my $debug         = 0;

my $backend =
      ( $^O eq 'MSWin32' ) ? 'Net::Rendezvous::Publish::Backend::BonjourWin32'
    : ( $^O =~ /darwin/ ) ? 'Net::Rendezvous::Publish::Backend::Apple'
    : q{};

GetOptions(
    'p|port=i'          => \$local_port,
    'n|name=s'          => \$local_name,
    'r|remote-server=s' => \$remote_server,
    'remote-port=i'     => \$remote_port,
    'backend=s'         => \$backend,
    'd|debug'           => \$debug,
    'h|help'            => \$help,
);

pod2usage(1) if $help;

sub forwarder_create {
    my ( $handle, $peer_host, $peer_port, $remote_addr, $remote_port ) = @_;

    POE::Session->create(
        inline_states => {
            _start         => \&forwarder_start,
            _stop          => \&forwarder_stop,
            client_input   => \&forwarder_client_input,
            client_error   => \&forwarder_client_error,
            server_connect => \&forwarder_server_connect,
            server_input   => \&forwarder_server_input,
            server_error   => \&forwarder_server_error,
        },

        args =>
            [ $handle, $peer_host, $peer_port, $remote_addr, $remote_port ]
    );
}

sub forwarder_start {
    my ( $heap, $session, $socket, $peer_host, $peer_port, $remote_addr,
        $remote_port )
        = @_[ HEAP, SESSION, ARG0, ARG1, ARG2, ARG3, ARG4 ];

    $heap->{log}           = $session->ID;
    $peer_host             = inet_ntoa($peer_host);
    $heap->{peer_host}     = $peer_host;
    $heap->{peer_port}     = $peer_port;
    $heap->{remote_addr}   = $remote_addr;
    $heap->{remote_port}   = $remote_port;

    print "[$heap->{log}] Accepted connection from $peer_host:$peer_port\n"
        if $debug;

    $heap->{state} = 'connecting';
    $heap->{queue} = [];

    $heap->{ka_client}    = POE::Component::Client::Keepalive->new;
    $heap->{wheel_client} = POE::Component::Connection::Keepalive->new(
        socket  => $socket,
        manager => $heap->{ka_client},
    )->start(
        Driver     => POE::Driver::SysRW->new,
        Filter     => POE::Filter::Stream->new,
        InputEvent => 'client_input',
        ErrorEvent => 'client_error',
    );

    $heap->{wheel_server} = POE::Wheel::SocketFactory->new(
        RemoteAddress => $remote_addr,
        RemotePort    => $remote_port,
        SuccessEvent  => 'server_connect',
        FailureEvent  => 'server_error',
    );
}

sub forwarder_stop {
    my $heap = $_[HEAP];
    print "[$heap->{log}] Closing redirection session\n"
        if $debug;
}

sub forwarder_client_input {
    my ( $heap, $input ) = @_[ HEAP, ARG0 ];

    if ( $input =~ /^([\040-\377\r\n\t]*)$/s ) {
        print '[client_input] ' . $input . "\n" if $debug;
    }

    if ( $heap->{state} eq 'connecting' ) {
        push @{ $heap->{queue} }, $input;
    }
    else {
        ( exists $heap->{wheel_server} )
            && $heap->{wheel_server}->put($input);
    }
}

sub forwarder_client_error {
    my ( $kernel, $heap, $operation, $errnum, $errstr )
        = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ];

    if ($errnum) {
        print(
            "[$heap->{log}] Client connection encountered ",
            "$operation error $errnum: $errstr\n"
            )
            if $debug;
    }
    else {
        print "[$heap->{log}] Client closed connection.\n"
            if $debug;
    }

    delete $heap->{wheel_client};
    delete $heap->{wheel_server};
}

sub forwarder_server_connect {
    my ( $kernel, $session, $heap, $socket )
        = @_[ KERNEL, SESSION, HEAP, ARG0 ];

    my ( $local_port, $local_addr )
        = unpack_sockaddr_in( getsockname($socket) );
    $local_addr = inet_ntoa($local_addr);
    print(
        "[$heap->{log}] Established forward from local ",
        "$local_addr:$local_port to remote ",
        $heap->{remote_addr},
        ':',
        $heap->{remote_port},
        "\n"
        )
        if $debug;

    # Replace the SocketFactory wheel with a ReadWrite wheel.
    $heap->{socket}       = $socket;
    $heap->{ka_server}    = POE::Component::Client::Keepalive->new;
    $heap->{wheel_server} = POE::Component::Connection::Keepalive->new(
        socket  => $socket,
        manager => $heap->{ka_server},
    )->start(
        Driver     => POE::Driver::SysRW->new,
        Filter     => POE::Filter::Stream->new,
        InputEvent => 'server_input',
        ErrorEvent => 'server_error',
    );

    $heap->{state} = 'connected';
    foreach my $pending ( @{ $heap->{queue} } ) {
        $kernel->call( $session, 'client_input', $pending );
    }
    $heap->{queue} = [];
}

sub forwarder_server_input {
    my ( $heap, $input ) = @_[ HEAP, ARG0 ];

    if ( $input =~ /^([\040-\377\r\n\t]*)$/s ) {
        print '[server input] ' . $input . "\n" if $debug;
    }

    exists( $heap->{wheel_client} ) and $heap->{wheel_client}->put($input);
}

sub forwarder_server_error {
    my ( $kernel, $heap, $operation, $errnum, $errstr )
        = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ];

    if ($errnum) {
        print(
            "[$heap->{log}] Server connection encountered ",
            "$operation error $errnum: $errstr\n"
            )
            if $debug;
        delete $heap->{wheel_client};
        delete $heap->{wheel_server};
    }
}

sub server_create {
    my ( $local_address, $local_port, $remote_address, $remote_port ) = @_;

    POE::Session->create(
        inline_states => {
            _start         => \&server_start,
            _stop          => \&server_stop,
            accept_success => \&server_accept_success,
            accept_failure => \&server_accept_failure,
        },

        # Pass this function's parameters to the server_start().
        #         ARG0,            ARG1,        ARG2,            ARG3
        args => [ $local_address, $local_port, $remote_address, $remote_port ]
    );
}

sub server_start {
    my ( $heap, $local_addr, $local_port, $remote_addr, $remote_port )
        = @_[ HEAP, ARG0, ARG1, ARG2, ARG3 ];

    print
        "+ Redirecting $local_addr:$local_port to $remote_addr:$remote_port\n"
        if $debug;

    $heap->{local_addr}  = $local_addr;
    $heap->{local_port}  = $local_port;
    $heap->{remote_addr} = $remote_addr;
    $heap->{remote_port} = $remote_port;

    $heap->{server_wheel} = POE::Wheel::SocketFactory->new(
        BindPort     => $local_port,       # and bind to this port
        Reuse        => 'yes',             # reuse immediately
        SuccessEvent => 'accept_success',  # generate this event on connection
        FailureEvent => 'accept_failure',  # generate this event on error
    );
}

sub server_stop {
    my $heap = $_[HEAP];
    print(
        "- Redirection from $heap->{local_addr}:$heap->{local_port} to ",
        "$heap->{remote_addr}:$heap->{remote_port} has stopped.\n"
        )
        if $debug;
}

sub server_accept_success {
    my ( $heap, $socket, $peer_addr, $peer_port )
        = @_[ HEAP, ARG0, ARG1, ARG2 ];
    &forwarder_create( $socket, $peer_addr, $peer_port, $heap->{remote_addr},
        $heap->{remote_port} );
}

sub server_accept_failure {
    my ( $heap, $operation, $errnum, $errstr ) = @_[ HEAP, ARG0, ARG1, ARG2 ];

    print(
        "! Redirection from $heap->{local_addr}:$heap->{local_port} to ",
        "$heap->{remote_addr}:$heap->{remote_port} encountered $operation ",
        "error $errnum: $errstr\n"
        )
        if $debug;

    delete $heap->{server_wheel} if $errnum == ENFILE or $errnum == EMFILE;
}

server_create( 'localhost', $local_port, $remote_server, $remote_port );

my $service = Net::Rendezvous::Publish->new( backend => $backend )->publish(
    name => $local_name,
    type => '_daap._tcp',
    port => $local_port,
);

$poe_kernel->run;

=head1 NAME

daap_proxy.pl - Simple DAAP Proxy

=head1 SYNOPSIS

daap_proxy.pl [options]

 Options:
   -p, --port [port]     Port number to use (default: 9999)
   -n, --name [name]     Server Name for Bonjour (default: daap-proxy)
   -r, --remote-server [server]
                         Remote Server address (default: localhost)
       --remote-port [port]
                         Remote Server port (default: 3689)
       --backend         Net::Rendezvous::Publich::Backend::*
                         (default:
                            Net::Rendezvous::Publish::Backend::Boujour32 on Win32
                                                               Apple on OSX )
   -d, --debug           Show debug messages
   -h, --help            Show this help

=head1 SEE ALSO

POE: POE Cookbook/TCP Port Redirection
http://poe.perl.org/?POE_Cookbook/TCP_Port_Redirection

=head1 AUTHOR

Daisuke Murase E<lt>typester@cpan.orgE<gt>

=cut

1;

